CH8. 신용카드 거래에 대한 그래프 분석(frac=0.3)

graph
Author

김보람

Published

April 4, 2023

ref

신용카드 거래에 대한 그래프 분석

  • 신용카드 거래 그래프 생성

  • 그래프에서 속성 및 커뮤니티 추출

  • 사기 거래 분류에 지도 및 비지도 머신러닝 알고리즘 적용

import pandas as pd
import os
import math
import numpy as np
import networkx as nx
import matplotlib.pyplot as plt
%matplotlib inline

default_edge_color = 'gray'
default_node_color = '#407cc9'
enhanced_node_color = '#f5b042'
enhanced_edge_color = '#cc2f04'

샘플 = 0.3

import pandas as pd
df = pd.read_csv("fraudTrain.csv")
df = df[df["is_fraud"]==0].sample(frac=0.30, random_state=42).append(df[df["is_fraud"] == 1])
df.head()
Unnamed: 0 trans_date_trans_time cc_num merchant category amt first last gender street ... lat long city_pop job dob trans_num unix_time merch_lat merch_long is_fraud
669418 669418 2019-10-12 18:21 4.089100e+18 fraud_Haley, Jewess and Bechtelar shopping_pos 7.53 Debra Stark F 686 Linda Rest ... 32.3836 -94.8653 24536 Multimedia programmer 1983-10-14 d313353fa30233e5fab5468e852d22fc 1350066071 32.202008 -94.371865 0
32567 32567 2019-01-20 13:06 4.247920e+12 fraud_Turner LLC travel 3.79 Judith Moss F 46297 Benjamin Plains Suite 703 ... 39.5370 -83.4550 22305 Television floor manager 1939-03-09 88c65b4e1585934d578511e627fe3589 1327064760 39.156673 -82.930503 0
156587 156587 2019-03-24 18:09 4.026220e+12 fraud_Klein Group entertainment 59.07 Debbie Payne F 204 Ashley Neck Apt. 169 ... 41.5224 -71.9934 4720 Broadcast presenter 1977-05-18 3bd9ede04b5c093143d5e5292940b670 1332612553 41.657152 -72.595751 0
1020243 1020243 2020-02-25 15:12 4.957920e+12 fraud_Monahan-Morar personal_care 25.58 Alan Parsons M 0547 Russell Ford Suite 574 ... 39.6171 -102.4776 207 Network engineer 1955-12-04 19e16ee7a01d229e750359098365e321 1361805120 39.080346 -103.213452 0
116272 116272 2019-03-06 23:19 4.178100e+15 fraud_Kozey-Kuhlman personal_care 84.96 Jill Flores F 639 Cruz Islands ... 41.9488 -86.4913 3104 Horticulturist, commercial 1981-03-29 a0c8641ca1f5d6e243ed5a2246e66176 1331075954 42.502065 -86.732664 0

5 rows × 23 columns

df.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 318777 entries, 669418 to 1047918
Data columns (total 23 columns):
 #   Column                 Non-Null Count   Dtype  
---  ------                 --------------   -----  
 0   Unnamed: 0             318777 non-null  int64  
 1   trans_date_trans_time  318777 non-null  object 
 2   cc_num                 318777 non-null  float64
 3   merchant               318777 non-null  object 
 4   category               318777 non-null  object 
 5   amt                    318777 non-null  float64
 6   first                  318777 non-null  object 
 7   last                   318777 non-null  object 
 8   gender                 318777 non-null  object 
 9   street                 318777 non-null  object 
 10  city                   318777 non-null  object 
 11  state                  318777 non-null  object 
 12  zip                    318777 non-null  int64  
 13  lat                    318777 non-null  float64
 14  long                   318777 non-null  float64
 15  city_pop               318777 non-null  int64  
 16  job                    318777 non-null  object 
 17  dob                    318777 non-null  object 
 18  trans_num              318777 non-null  object 
 19  unix_time              318777 non-null  int64  
 20  merch_lat              318777 non-null  float64
 21  merch_long             318777 non-null  float64
 22  is_fraud               318777 non-null  int64  
dtypes: float64(6), int64(5), object(12)
memory usage: 58.4+ MB
df.isnull().sum()
Unnamed: 0               0
trans_date_trans_time    0
cc_num                   0
merchant                 0
category                 0
amt                      0
first                    0
last                     0
gender                   0
street                   0
city                     0
state                    0
zip                      0
lat                      0
long                     0
city_pop                 0
job                      0
dob                      0
trans_num                0
unix_time                0
merch_lat                0
merch_long               0
is_fraud                 0
dtype: int64
df["is_fraud"].value_counts()
0    312771
1      6006
Name: is_fraud, dtype: int64
df["is_fraud"].value_counts()/len(df)
0    0.981159
1    0.018841
Name: is_fraud, dtype: float64
m
_df = pd.read_csv("fraudTrain.csv")
NameError: name 'pd' is not defined
_df.columns
NameError: name '_df' is not defined
cus_list = set(_df.query('is_fraud==1').cc_num.tolist())
_df2 = _df.query("cc_num in @ cus_list")
_df2.shape
(651430, 23)
_df2.columns
Index(['Unnamed: 0', 'trans_date_trans_time', 'cc_num', 'merchant', 'category',
       'amt', 'first', 'last', 'gender', 'street', 'city', 'state', 'zip',
       'lat', 'long', 'city_pop', 'job', 'dob', 'trans_num', 'unix_time',
       'merch_lat', 'merch_long', 'is_fraud'],
      dtype='object')
_df2.groupby('is_fraud').agg({'category':np.sum})

- 이분그래프

def build_graph_bipartite(df_input, graph_type=nx.Graph()):
    df=df_input.copy()
    mapping={x:node_id for node_id, x in enumerate(set(df["cc_num"].values.tolist()+\
                                                      df["merchant"].values.tolist()))}
    
    df["from"]=df["cc_num"].apply(lambda x:mapping[x])  #엣지의 출발점
    df["to"]=df["merchant"].apply(lambda x:mapping[x])  #엣지의 도착점
    
    df = df[['from', 'to', "amt", "is_fraud"]].groupby(['from','to']).agg({"is_fraud":"sum","amt":"sum"}).reset_index()
    df["is_fraud"]=df["is_fraud"].apply(lambda x:1 if x>0 else 0)
    
    G=nx.from_edgelist(df[["from","to"]].values, create_using=graph_type)
    
    nx.set_edge_attributes(G, {(int(x["from"]),int(x["to"])):x["is_fraud"] for idx, x in df[["from","to","is_fraud"]].iterrows()}, "label")  #엣지 속성 설정,각 속성의 사기 여부부 
    
    nx.set_edge_attributes(G,{(int(x["from"]),int(x["to"])):x["amt"] for idx,x in df[["from","to","amt"]].iterrows()}, "weight") # 엣지 속성 설정, 각 엣지의 거래 금액

    return G
  • 판매자, 고객에게 node 할당
G_bu = build_graph_bipartite(df, nx.Graph(name="Bipartite Undirect"))
KeyboardInterrupt: 
  • 무향 그래프 작성

- 삼분그래프

def build_graph_tripartite(df_input, graph_type=nx.Graph()):
    df=df_input.copy()
    mapping={x:node_id for node_id, x in enumerate(set(df.index.values.tolist() + #set으로 중복 제거
                                                       df["cc_num"].values.tolist() +
                                                       df["merchant"].values.tolist()))}
    df["in_node"]= df["cc_num"].apply(lambda x: mapping[x])
    df["out_node"]=df["merchant"].apply(lambda x:mapping[x])
    
        
    G=nx.from_edgelist([(x["in_node"], mapping[idx]) for idx, x in df.iterrows()] +\
                        [(x["out_node"], mapping[idx]) for idx, x in df.iterrows()], create_using=graph_type)
    
    nx.set_edge_attributes(G,{(x["in_node"], mapping[idx]):x["is_fraud"] for idx, x in df.iterrows()}, "label")
     
    nx.set_edge_attributes(G,{(x["out_node"], mapping[idx]):x["is_fraud"] for idx, x in df.iterrows()}, "label")
    
    nx.set_edge_attributes(G,{(x["in_node"], mapping[idx]):x["amt"] for idx, x in df.iterrows()}, "weight")
    
    nx.set_edge_attributes(G,{(x["out_node"], mapping[idx]):x["amt"] for idx, x in df.iterrows()}, "weight")
    
    
    return G
    
  • 판매자, 고객, 거래에 노드 할당
G_tu = build_graph_tripartite(df, nx.Graph())
for G in [G_bu, G_tu]:
    print(nx.number_of_nodes(G))
1636
320413
  • smaple=30%로 하니까 삼분그래프의 노드수가 증가했다.
for G in [G_bu, G_tu]:
    print(nx.number_of_edges(G))
228436
637554
  • 엣지수는 이분, 삼분그래프 둘다 증가

네트워크 토폴로지

  • 각 그래프별 차수 분포 살펴보기
for G in [G_bu, G_tu]:
    plt.figure(figsize=(10,10))
    degrees = pd.Series({k:v for k, v in nx.degree(G)})
    degrees.plot.hist()
    plt.yscale("log")

  • x축: 노드의 연결도

  • y축: 로그 스케일(연결도가 큰 노드의 수가 매우 적으므로)

- 각 그래프 간선 가중치 분포

for G in [G_bu, G_tu]:
    allEdgeWeights = pd.Series({
        (d[0],d[1]):d[2]["weight"]  #d[0],d[1]을 key로 d[2]를 weight로
        #d는 G.edges(data=True)로 (u,v,data)형태의 튜플을 반복하는 반복문
        for d in G.edges(data=True)})
    np.quantile(allEdgeWeights.values,
               [0.10, 0.50, 0.70, 0.9])
    
np.quantile(allEdgeWeights.values,[0.10, 0.50, 0.70, 0.9])
array([  4.15,  48.01,  75.75, 141.91])

- 매게 중심성 측정 지표

for G in [G_bu, G_tu]:
    plt.figure(figsize=(10,10))
    bc_distr = pd.Series(nx.betweenness_centrality(G))
    bc_distr.plot.hist()
    plt.yscale("log")
KeyboardInterrupt: 
<Figure size 1000x1000 with 0 Axes>
  • 그래프 내에서 노드가 얼마나 중심적인 역할을 하는지 나타내는 지표

  • 해당 노드가 얼마나 많은 최단경로에 포함되는지 살피기

  • 노드가 많은 최단경로를 포함하면 해당노드의 매개중심성은 커진다.

- 상관계수

for G in [G_bu, G_tu]:
    print(nx.degree_pearson_correlation_coefficient(G))
-0.12467174727090688
-0.8051895351325623
  • 음의 동류성(서로 다른 속성을 가진 노드들끼리 연결되어 있다.)

  • 0~ -1 사이의 값을 가짐

  • -1에 가까울수록 서로 다른 속성을 가진 노드들끼리 강한 음의 상관관계

  • 0에 가까울수록 노드들이 연결될 때 서로 다른 속성을 가진 노드들끼리 큰 차이가 없음 =>

  • 연결도 높은 개인이 연골도 낮은 개인과 연관돼 있다.

  • 이분그래프: 낮은 차수의 고객은 들어오는 트랜잭션 수가 많은 높은 차수의 판매자와만 연결되어 상관계수가 낮다.

  • 삼분그래프:동류성이 훨씬 더 낮다. 트랜잭션 노드가 있기 댸문에?

커뮤니티 감지

# pip install python-louvain
import networkx as nx
import community
import community
for G in [G_bu, G_tu]:
    parts = community.best_partition(G, random_state=42, weight='weight')
communities = pd.Series(parts)
communities
255288    72
204367    72
65143     92
10004     23
194072     3
          ..
286119    78
194740    88
53644     57
300283     9
313041    66
Length: 320413, dtype: int64
print(communities.value_counts().sort_values(ascending=False))
4      9426
94     6025
6      5835
42     5636
50     5016
       ... 
112    1341
91     1307
18     1104
62     1057
85      585
Length: 113, dtype: int64
  • 커뮤니티 종류가 늘었따. 96>>113개로

  • 커뮤니티 감지를 통해 특정 사기 패턴 식별

  • 커뮤니티 추출 후 포함된 노드 수에 따라 정렬

communities.value_counts().plot.hist(bins=20)

  • 9426개 이상한거 하나있고.. 약간 2000~3000사이에 집중되어 보인다.
graphs = [] # 부분그래프 저장
d = {}  # 부정 거래 비율 저장 
for x in communities.unique():
    tmp = nx.subgraph(G, communities[communities==x].index)
    fraud_edges = sum(nx.get_edge_attributes(tmp, "label").values())
    ratio = 0 if fraud_edges == 0 else (fraud_edges/tmp.number_of_edges())*100
    d[x] = ratio
    graphs += [tmp]

pd.Series(d).sort_values(ascending=False)
56     5.281326
59     4.709632
111    4.399142
77     4.149798
15     3.975843
         ...   
90     0.409650
112    0.297398
110    0.292826
67     0.277008
18     0.180180
Length: 113, dtype: float64
  • 사기 거래 비율 계산. 사기 거래가 집중된 특정 하위 그래프 식별

  • 특정 커뮤니티에 포함된 노드를 사용하여 노드 유도 하위 그래프 생성

  • 하위 그래프: 모든 간선 수에 대한 사기 거래 간선 수의 비율로 사기 거래 백분율 계싼

gId = 10
plt.figure(figsize=(10,10))
spring_pos = nx.spring_layout(graphs[gId])
plt.axis("off")
edge_colors = ["r" if x == 1 else "g" for x in nx.get_edge_attributes(graphs[gId], 'label').values()]  #r:빨간색, g:녹색
nx.draw_networkx(graphs[gId], pos=spring_pos, node_color=default_node_color, 
                 edge_color=edge_colors, with_labels=False, node_size=15)

  • 커뮤니티 감지 알고리즘에 의해 감지된 노드 유도 하위 그래프 그리기

  • 특정 커뮤니티 인덱스 gId가 주어지면 해당 커뮤니티에서 사용 가능한 노드로 유도 하위 그래프 추출하고 얻는다.

gId = 56
plt.figure(figsize=(10,10))
spring_pos = nx.spring_layout(graphs[gId])
plt.axis("off")
edge_colors = ["r" if x == 1 else "g" for x in nx.get_edge_attributes(graphs[gId], 'label').values()]  #r:빨간색, g:녹색
nx.draw_networkx(graphs[gId], pos=spring_pos, node_color=default_node_color, 
                 edge_color=edge_colors, with_labels=False, node_size=15)

gId = 18
plt.figure(figsize=(10,10))
spring_pos = nx.spring_layout(graphs[gId])
plt.axis("off")
edge_colors = ["r" if x == 1 else "g" for x in nx.get_edge_attributes(graphs[gId], 'label').values()]  #r:빨간색, g:녹색
nx.draw_networkx(graphs[gId], pos=spring_pos, node_color=default_node_color, 
                 edge_color=edge_colors, with_labels=False, node_size=15)

pd.Series(d).plot.hist(bins=20)

사기 탐지를 위한 지도 및 비지도 임베딩

  • 트랜잭션 간선으로 표기

  • 각 간선을 올바른 클래스(사기 또는 정상)으로 분류

지도학습

from sklearn.utils import resample

df_majority = df[df.is_fraud==0]
df_minority = df[df.is_fraud==1]

df_maj_dowsampled = resample(df_majority,
                             n_samples=len(df_minority),
                             random_state=42)

df_downsampled = pd.concat([df_minority, df_maj_dowsampled])

print(df_downsampled.is_fraud.value_counts())
G_down = build_graph_bipartite(df_downsampled)
1    6006
0    6006
Name: is_fraud, dtype: int64
df.shape
(318777, 23)
df_minority.shape
(6006, 23)
df_majority.shape
(312771, 23)
6006 / 312771 
0.019202547550763976
  • 무작위 언더샘플링 사용

  • 소수 클래스(사기거래)이 샘플 수 와 일치시키려고 다수 클래스(정상거래)의 하위 샘플을 가져옴

  • 데이터 불균형을 처리하기 위해서

from sklearn.model_selection import train_test_split


train_edges, test_edges, train_labels, test_labels = train_test_split(list(range(len(G_down.edges))), 
                                                                      list(nx.get_edge_attributes(G_down, "label").values()), 
                                                                      test_size=0.20, 
                                                                      random_state=42)
edgs = list(G_down.edges)
train_graph = G_down.edge_subgraph([edgs[x] for x in train_edges]).copy()
train_graph.add_nodes_from(list(set(G_down.nodes) - set(train_graph.nodes)))
  • 데이터 8:2 비율로 학습 검증
from node2vec import Node2Vec
from node2vec.edges import HadamardEmbedder, AverageEmbedder, WeightedL1Embedder, WeightedL2Embedder

node2vec_train = Node2Vec(train_graph, weight_key='weight')
model_train = node2vec_train.fit(window=10)
Generating walks (CPU: 1):   0%|          | 0/10 [00:00<?, ?it/s]Generating walks (CPU: 1): 100%|██████████| 10/10 [00:04<00:00,  2.45it/s]
  • Node2Vec 알고리즘 사용해 특징 공간 구축
from sklearn.ensemble import RandomForestClassifier 
from sklearn import metrics 

classes = [HadamardEmbedder, AverageEmbedder, WeightedL1Embedder, WeightedL2Embedder]
for cl in classes:
    embeddings_train = cl(keyed_vectors=model_train.wv) 
    # 벡터스페이스 상에 edge를 투영.. 

    train_embeddings = [embeddings_train[str(edgs[x][0]), str(edgs[x][1])] for x in train_edges]
    test_embeddings = [embeddings_train[str(edgs[x][0]), str(edgs[x][1])] for x in test_edges]
    
    rf = RandomForestClassifier(n_estimators=1000, random_state=42) 
    rf.fit(train_embeddings, train_labels); 
    #
    y_hat = rf.predict_proba(test_embeddings)
    y_pred = np.argmax(yhat,axis=1)
    #y_pred = rf.predict(test_embeddings)
    print(cl)
    print('Precision:', metrics.precision_score(test_labels, y_pred)) 
    print('Recall:', metrics.recall_score(test_labels, y_pred)) 
    print('F1-Score:', metrics.f1_score(test_labels, y_pred)) 
<class 'node2vec.edges.HadamardEmbedder'>
Precision: 0.7236842105263158
Recall: 0.1407849829351536
F1-Score: 0.2357142857142857
y_pred
array([1, 0, 0, ..., 0, 0, 0])
yhat = rf.predict_proba(test_embeddings)
yhat
array([[0.457, 0.543],
       [0.634, 0.366],
       [0.609, 0.391],
       ...,
       [0.577, 0.423],
       [0.59 , 0.41 ],
       [0.557, 0.443]])

비지도학습

  • k-means 알고리즘 사용

  • 지도학습과의 차이점은 특징 공간이 학습-검증 분할을 안함.

nod2vec_unsup = Node2Vec(G_down, weight_key='weight')
unsup_vals = nod2vec_unsup.fit(window=10)
Generating walks (CPU: 1): 100%|██████████| 10/10 [00:04<00:00,  2.30it/s]
  • 다운샘플링 절차에 전체 그래프 알고리즘 계산
from sklearn.cluster import KMeans

classes = [HadamardEmbedder, AverageEmbedder, WeightedL1Embedder, WeightedL2Embedder]
true_labels = [x for x in nx.get_edge_attributes(G_down, "label").values()]

for cl in classes:
    embedding_edge = cl(keyed_vectors=unsup_vals.wv) 

    embedding = [embedding_edge[str(x[0]), str(x[1])] for x in G_down.edges()]
    kmeans = KMeans(2, random_state=42).fit(embedding)
    
    
    nmi = metrics.adjusted_mutual_info_score(true_labels, kmeans.labels_)
    ho = metrics.homogeneity_score(true_labels, kmeans.labels_)
    co = metrics.completeness_score(true_labels, kmeans.labels_)
    vmeasure = metrics.v_measure_score(true_labels, kmeans.labels_)
    
    print(cl)
    print('NMI:', nmi)
    print('Homogeneity:', ho)
    print('Completeness:', co)
    print('V-Measure:', vmeasure)
/home/coco/anaconda3/envs/py38/lib/python3.8/site-packages/sklearn/cluster/_kmeans.py:870: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning
  warnings.warn(
/home/coco/anaconda3/envs/py38/lib/python3.8/site-packages/sklearn/cluster/_kmeans.py:870: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning
  warnings.warn(
/home/coco/anaconda3/envs/py38/lib/python3.8/site-packages/sklearn/cluster/_kmeans.py:870: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning
  warnings.warn(
/home/coco/anaconda3/envs/py38/lib/python3.8/site-packages/sklearn/cluster/_kmeans.py:870: FutureWarning: The default value of `n_init` will change from 10 to 'auto' in 1.4. Set the value of `n_init` explicitly to suppress the warning
  warnings.warn(
<class 'node2vec.edges.HadamardEmbedder'>
NMI: 0.04418691434534317
Homogeneity: 0.0392170155918133
Completeness: 0.05077340984619601
V-Measure: 0.044253187956299615
<class 'node2vec.edges.AverageEmbedder'>
NMI: 0.10945180042668563
Homogeneity: 0.10590886334115046
Completeness: 0.11336117407653773
V-Measure: 0.10950837820667877
<class 'node2vec.edges.WeightedL1Embedder'>
NMI: 0.17575054988974667
Homogeneity: 0.1757509360433583
Completeness: 0.17585150874409544
V-Measure: 0.17580120800977098
<class 'node2vec.edges.WeightedL2Embedder'>
NMI: 0.13740583375677415
Homogeneity: 0.13628828058562012
Completeness: 0.1386505946822449
V-Measure: 0.13745928896382234

- NMI(Normalized Mutual Information)

  • 두 개의 군집 결과 비교

  • 0~1이며 1에 가까울수록 높은 성능

- Homogeneity

  • 하나의 실제 군집 내에서 같은 군집에 속한 샘플들이 군집화 결과에서 같은 군집에 속할 비율

  • 1에 가까울수록 높은 성능

- Completeness

  • 하나의 예측 군집 내에서 같은 실제 군집에 속한 샘플들이 군집화 결과에서 같은 군집에 속할 비율

  • 0~1이며 1에 가까울수록 높은 성능

- V-measure

  • Homogeneity와 Completeness의 조화 평균

  • 0~1이며 1에 가까울수록 높은 성능

  • 비지도 학습에 이상치 탐지 방법

  • k-means/LOF/One-class SVM 등이 있다.. 한번 같이 해보자.

  • 조금씩 다 커졌넹..

- 지도학습에서 정상거래에서 다운샘플링을 했는데

만약, 사기거래에서 업샘플링을 하게되면 어떻게 될까?